﻿/***************************************************************************

Copyright (c) Microsoft Corporation 2012-2015.

This code is licensed using the Microsoft Public License (Ms-PL).  The text of the license can be found here:

http://www.microsoft.com/resources/sharedsource/licensingbasics/publiclicense.mspx

Published at http://OpenXmlDeveloper.org
Resource Center and Documentation: http://openxmldeveloper.org/wiki/w/wiki/powertools-for-open-xml.aspx

Developer: Eric White
Blog: http://www.ericwhite.com
Twitter: @EricWhiteDev
Email: eric@ericwhite.com

Version: 2.6.00
 * Enhancements to support HtmlConverter.cs

***************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;

namespace OpenXmlPowerTools
{
    public static class PtUtils
    {
        public static string NormalizeDirName(string dirName)
        {
            string d = dirName.Replace('\\', '/');
            if (d[dirName.Length - 1] != '/' && d[dirName.Length - 1] != '\\')
                return d + "/";

            return d;
        }

        public static string MakeValidXml(string p)
        {
            return p.Any(c => c < 0x20)
                ? p.Select(c => c < 0x20 ? string.Format("_{0:X}_", (int) c) : c.ToString()).StringConcatenate()
                : p;
        }

        public static void AddElementIfMissing(XDocument partXDoc, XElement existing, string newElement)
        {
            if (existing != null)
                return;

            XElement newXElement = XElement.Parse(newElement);
            newXElement.Attributes().Where(a => a.IsNamespaceDeclaration).Remove();
            if (partXDoc.Root != null) partXDoc.Root.Add(newXElement);
        }
    }

    public class Normalizer
    {
        public static XDocument Normalize(XDocument source/*, XmlSchemaSet schema*/)
        {
            bool havePSVI = false;
            // validate, throw errors, add PSVI information
            //if (schema != null)
            //{
            //    source.Validate(schema, null, true);
            //    havePSVI = true;
           // }
            return new XDocument(
                source.Declaration,
                source.Nodes().Select(n =>
                {
                    // Remove comments, processing instructions, and text nodes that are
                    // children of XDocument.  Only white space text nodes are allowed as
                    // children of a document, so we can remove all text nodes.
                    if (n is XComment || n is XProcessingInstruction || n is XText)
                        return null;
                    XElement e = n as XElement;
                    if (e != null)
                        return NormalizeElement(e, havePSVI);
                    return n;
                }
                )
            );
        }

        public static bool DeepEqualsWithNormalization(XDocument doc1, XDocument doc2
            /*XmlSchemaSet schemaSet*/)
        {
            XDocument d1 = Normalize(doc1);
            XDocument d2 = Normalize(doc2);
            return XNode.DeepEquals(d1, d2);
        }

        private static IEnumerable<XAttribute> NormalizeAttributes(XElement element,
            bool havePSVI)
        {
            return element.Attributes()
                    .Where(a => !a.IsNamespaceDeclaration &&
                        a.Name != Xsi.schemaLocation &&
                        a.Name != Xsi.noNamespaceSchemaLocation)
                    .OrderBy(a => a.Name.NamespaceName)
                    .ThenBy(a => a.Name.LocalName)
                    .Select(
                        a =>
                        {
                            //if (havePSVI)
                            //{
                            //    var dt = a.GetSchemaInfo().SchemaType.TypeCode;
                            //    switch (dt)
                            //    {
                            //        case XmlTypeCode.Boolean:
                            //            return new XAttribute(a.Name, (bool)a);
                            //        case XmlTypeCode.DateTime:
                            //            return new XAttribute(a.Name, (DateTime)a);
                            //        case XmlTypeCode.Decimal:
                            //            return new XAttribute(a.Name, (decimal)a);
                            //        case XmlTypeCode.Double:
                            //            return new XAttribute(a.Name, (double)a);
                            //        case XmlTypeCode.Float:
                            //            return new XAttribute(a.Name, (float)a);
                            //        case XmlTypeCode.HexBinary:
                            //        case XmlTypeCode.Language:
                            //            return new XAttribute(a.Name,
                            //                ((string)a).ToLower());
                            //    }
                            //}
                            return a;
                        }
                    );
        }

        private static XNode NormalizeNode(XNode node, bool havePSVI)
        {
            // trim comments and processing instructions from normalized tree
            if (node is XComment || node is XProcessingInstruction)
                return null;
            XElement e = node as XElement;
            if (e != null)
                return NormalizeElement(e, havePSVI);
            // Only thing left is XCData and XText, so clone them
            return node;
        }

        private static XElement NormalizeElement(XElement element, bool havePSVI)
        {
            //if (havePSVI)
            //{
            //    var dt = element.GetSchemaInfo();
            //    switch (dt.SchemaType.TypeCode)
            //    {
            //        case XmlTypeCode.Boolean:
            //            return new XElement(element.Name,
            //                NormalizeAttributes(element, havePSVI),
            //                (bool)element);
            //        case XmlTypeCode.DateTime:
            //            return new XElement(element.Name,
            //                NormalizeAttributes(element, havePSVI),
            //                (DateTime)element);
            //        case XmlTypeCode.Decimal:
            //            return new XElement(element.Name,
            //                NormalizeAttributes(element, havePSVI),
            //                (decimal)element);
            //        case XmlTypeCode.Double:
            //            return new XElement(element.Name,
            //                NormalizeAttributes(element, havePSVI),
            //                (double)element);
            //        case XmlTypeCode.Float:
            //            return new XElement(element.Name,
            //                NormalizeAttributes(element, havePSVI),
            //                (float)element);
            //        case XmlTypeCode.HexBinary:
            //        case XmlTypeCode.Language:
            //            return new XElement(element.Name,
            //                NormalizeAttributes(element, havePSVI),
            //                ((string)element).ToLower());
            //        default:
            //            return new XElement(element.Name,
            //                NormalizeAttributes(element, havePSVI),
            //                element.Nodes().Select(n => NormalizeNode(n, havePSVI))
            //            );
            //    }
            //}
            //else
            {
                return new XElement(element.Name,
                    NormalizeAttributes(element, havePSVI),
                    element.Nodes().Select(n => NormalizeNode(n, havePSVI))
                );
            }
        }
    }

    public class FileUtils
    {
        public static DirectoryInfo GetDateTimeStampedDirectoryInfo(string prefix)
        {
            DateTime now = DateTime.Now;
            string dirName =
                prefix +
                string.Format("-{0:00}-{1:00}-{2:00}-{3:00}{4:00}{5:00}", now.Year - 2000, now.Month, now.Day, now.Hour,
                    now.Minute, now.Second);
            return new DirectoryInfo(dirName);
        }

        public static FileInfo GetDateTimeStampedFileInfo(string prefix, string suffix)
        {
            DateTime now = DateTime.Now;
            string fileName =
                prefix +
                string.Format("-{0:00}-{1:00}-{2:00}-{3:00}{4:00}{5:00}", now.Year - 2000, now.Month, now.Day, now.Hour,
                    now.Minute, now.Second) +
                suffix;
            return new FileInfo(fileName);
        }

        public static void ThreadSafeCreateDirectory(DirectoryInfo dir)
        {
            while (true)
            {
                if (dir.Exists)
                    break;

                try
                {
                    dir.Create();
                    break;
                }
                catch (IOException)
                {
                    Task.Delay(50).Wait();
                }
            }
        }

        public static void ThreadSafeCopy(FileInfo sourceFile, FileInfo destFile)
        {
            while (true)
            {
                if (destFile.Exists)
                    break;

                try
                {
                    File.Copy(sourceFile.FullName, destFile.FullName);
                    break;
                }
                catch (IOException)
                {
                    Task.Delay(50).Wait();
                }
            }
        }

        public static void ThreadSafeCreateEmptyTextFileIfNotExist(FileInfo file)
        {
            while (true)
            {
                if (file.Exists)
                    break;

                try
                {
                    File.WriteAllText(file.FullName, "");
                    break;
                }
                catch (IOException)
                {
                    Task.Delay(50).Wait();
                }
            }
        }

#if !NET35
        internal static void ThreadSafeAppendAllLines(FileInfo file, string[] strings)
        {
            while (true)
            {
                try
                {
                    File.AppendAllLines(file.FullName, strings);
                    break;
                }
                catch (IOException)
                {
                    Task.Delay(50).Wait();
                }
            }
        }
#endif

        public static List<string> GetFilesRecursive(DirectoryInfo dir, string searchPattern)
        {
            var fileList = new List<string>();
            GetFilesRecursiveInternal(dir, searchPattern, fileList);
            return fileList;
        }

        private static void GetFilesRecursiveInternal(DirectoryInfo dir, string searchPattern, List<string> fileList)
        {
            fileList.AddRange(dir.GetFiles(searchPattern).Select(file => file.FullName));
            foreach (DirectoryInfo subdir in dir.GetDirectories())
                GetFilesRecursiveInternal(subdir, searchPattern, fileList);
        }

        public static List<string> GetFilesRecursive(DirectoryInfo dir)
        {
            var fileList = new List<string>();
            GetFilesRecursiveInternal(dir, fileList);
            return fileList;
        }

        private static void GetFilesRecursiveInternal(DirectoryInfo dir, List<string> fileList)
        {
            fileList.AddRange(dir.GetFiles().Select(file => file.FullName));
            foreach (DirectoryInfo subdir in dir.GetDirectories())
                GetFilesRecursiveInternal(subdir, fileList);
        }

        public static void CopyStream(Stream source, Stream target)
        {
            const int bufSize = 0x4096;
            var buf = new byte[bufSize];
            int bytesRead;
            while ((bytesRead = source.Read(buf, 0, bufSize)) > 0)
                target.Write(buf, 0, bytesRead);
        }
    }

    public static class PtExtensions
    {
        public static XElement GetXElement(this XmlNode node)
        {
            var xDoc = new XDocument();
            using (XmlWriter xmlWriter = xDoc.CreateWriter())
                node.WriteTo(xmlWriter);
            return xDoc.Root;
        }

        public static XmlNode GetXmlNode(this XElement element)
        {
            var xmlDoc = new XmlDocument();
            using (XmlReader xmlReader = element.CreateReader())
                xmlDoc.Load(xmlReader);
            return xmlDoc;
        }

        public static XDocument GetXDocument(this XmlDocument document)
        {
            var xDoc = new XDocument();
            using (XmlWriter xmlWriter = xDoc.CreateWriter())
                document.WriteTo(xmlWriter);

            XmlDeclaration decl = document.ChildNodes.OfType<XmlDeclaration>().FirstOrDefault();
            if (decl != null)
                xDoc.Declaration = new XDeclaration(decl.Version, decl.Encoding, decl.Standalone);

            return xDoc;
        }

        public static XmlDocument GetXmlDocument(this XDocument document)
        {
            var xmlDoc = new XmlDocument();
            using (XmlReader xmlReader = document.CreateReader())
            {
                xmlDoc.Load(xmlReader);
                if (document.Declaration != null)
                {
                    XmlDeclaration dec = xmlDoc.CreateXmlDeclaration(document.Declaration.Version,
                        document.Declaration.Encoding, document.Declaration.Standalone);
                    xmlDoc.InsertBefore(dec, xmlDoc.FirstChild);
                }
            }
            return xmlDoc;
        }

        public static string StringConcatenate(this IEnumerable<string> source)
        {
            return source.Aggregate(
                new StringBuilder(),
                (sb, s) => sb.Append(s),
                sb => sb.ToString());
        }

        public static string StringConcatenate<T>(this IEnumerable<T> source, Func<T, string> projectionFunc)
        {
            return source.Aggregate(
                new StringBuilder(),
                (sb, i) => sb.Append(projectionFunc(i)),
                sb => sb.ToString());
        }

        public static IEnumerable<TResult> PtZip<TFirst, TSecond, TResult>(
            this IEnumerable<TFirst> first,
            IEnumerable<TSecond> second,
            Func<TFirst, TSecond, TResult> func)
        {
            using (IEnumerator<TFirst> ie1 = first.GetEnumerator())
            using (IEnumerator<TSecond> ie2 = second.GetEnumerator())
                while (ie1.MoveNext() && ie2.MoveNext())
                    yield return func(ie1.Current, ie2.Current);
        }

        public static IEnumerable<IGrouping<TKey, TSource>> GroupAdjacent<TSource, TKey>(
            this IEnumerable<TSource> source,
            Func<TSource, TKey> keySelector)
        {
            TKey last = default(TKey);
            var haveLast = false;
            var list = new List<TSource>();

            foreach (TSource s in source)
            {
                TKey k = keySelector(s);
                if (haveLast)
                {
                    if (!k.Equals(last))
                    {
                        yield return new GroupOfAdjacent<TSource, TKey>(list, last);

                        list = new List<TSource> { s };
                        last = k;
                    }
                    else
                    {
                        list.Add(s);
                        last = k;
                    }
                }
                else
                {
                    list.Add(s);
                    last = k;
                    haveLast = true;
                }
            }
            if (haveLast)
                yield return new GroupOfAdjacent<TSource, TKey>(list, last);
        }

        private static void InitializeSiblingsReverseDocumentOrder(XElement element)
        {
            XElement prev = null;
            foreach (XElement e in element.Elements())
            {
                e.AddAnnotation(new SiblingsReverseDocumentOrderInfo { PreviousSibling = prev });
                prev = e;
            }
        }

        [SuppressMessage("ReSharper", "PossibleNullReferenceException")]
        public static IEnumerable<XElement> SiblingsBeforeSelfReverseDocumentOrder(
            this XElement element)
        {
            if (element.Annotation<SiblingsReverseDocumentOrderInfo>() == null)
                InitializeSiblingsReverseDocumentOrder(element.Parent);
            XElement current = element;
            while (true)
            {
                XElement previousElement = current
                    .Annotation<SiblingsReverseDocumentOrderInfo>()
                    .PreviousSibling;
                if (previousElement == null)
                    yield break;

                yield return previousElement;

                current = previousElement;
            }
        }

        private static void InitializeDescendantsReverseDocumentOrder(XElement element)
        {
            XElement prev = null;
            foreach (XElement e in element.Descendants())
            {
                e.AddAnnotation(new DescendantsReverseDocumentOrderInfo { PreviousElement = prev });
                prev = e;
            }
        }

        [SuppressMessage("ReSharper", "PossibleNullReferenceException")]
        public static IEnumerable<XElement> DescendantsBeforeSelfReverseDocumentOrder(
            this XElement element)
        {
            if (element.Annotation<DescendantsReverseDocumentOrderInfo>() == null)
                InitializeDescendantsReverseDocumentOrder(element.AncestorsAndSelf().Last());
            XElement current = element;
            while (true)
            {
                XElement previousElement = current
                    .Annotation<DescendantsReverseDocumentOrderInfo>()
                    .PreviousElement;
                if (previousElement == null)
                    yield break;

                yield return previousElement;

                current = previousElement;
            }
        }

        private static void InitializeDescendantsTrimmedReverseDocumentOrder(XElement element, XName trimName)
        {
            XElement prev = null;
            foreach (XElement e in element.DescendantsTrimmed(trimName))
            {
                e.AddAnnotation(new DescendantsTrimmedReverseDocumentOrderInfo { PreviousElement = prev });
                prev = e;
            }
        }

        [SuppressMessage("ReSharper", "PossibleNullReferenceException")]
        public static IEnumerable<XElement> DescendantsTrimmedBeforeSelfReverseDocumentOrder(
            this XElement element, XName trimName)
        {
            if (element.Annotation<DescendantsTrimmedReverseDocumentOrderInfo>() == null)
            {
                XElement ances = element.AncestorsAndSelf(W.txbxContent).FirstOrDefault() ??
                                 element.AncestorsAndSelf().Last();
                InitializeDescendantsTrimmedReverseDocumentOrder(ances, trimName);
            }

            XElement current = element;
            while (true)
            {
                XElement previousElement = current
                    .Annotation<DescendantsTrimmedReverseDocumentOrderInfo>()
                    .PreviousElement;
                if (previousElement == null)
                    yield break;

                yield return previousElement;

                current = previousElement;
            }
        }

        public static string ToStringNewLineOnAttributes(this XElement element)
        {
            var settings = new XmlWriterSettings
            {
                Indent = true,
                OmitXmlDeclaration = true,
                NewLineOnAttributes = true
            };
            var stringBuilder = new StringBuilder();
            using (var stringWriter = new StringWriter(stringBuilder))
            using (XmlWriter xmlWriter = XmlWriter.Create(stringWriter, settings))
                element.WriteTo(xmlWriter);
            return stringBuilder.ToString();
        }

        public static IEnumerable<XElement> DescendantsTrimmed(this XElement element,
            XName trimName)
        {
            return DescendantsTrimmed(element, e => e.Name == trimName);
        }

        public static IEnumerable<XElement> DescendantsTrimmed(this XElement element,
            Func<XElement, bool> predicate)
        {
            Stack<IEnumerator<XElement>> iteratorStack = new Stack<IEnumerator<XElement>>();
            iteratorStack.Push(element.Elements().GetEnumerator());
            while (iteratorStack.Count > 0)
            {
                while (iteratorStack.Peek().MoveNext())
                {
                    XElement currentXElement = iteratorStack.Peek().Current;
                    if (predicate(currentXElement))
                    {
                        yield return currentXElement;
                        continue;
                    }
                    yield return currentXElement;
                    iteratorStack.Push(currentXElement.Elements().GetEnumerator());
                }
                iteratorStack.Pop();
            }
        }

        public static IEnumerable<TResult> Rollup<TSource, TResult>(
            this IEnumerable<TSource> source,
            TResult seed,
            Func<TSource, TResult, TResult> projection)
        {
            TResult nextSeed = seed;
            foreach (TSource src in source)
            {
                TResult projectedValue = projection(src, nextSeed);
                nextSeed = projectedValue;
                yield return projectedValue;
            }
        }

        public static IEnumerable<TResult> Rollup<TSource, TResult>(
            this IEnumerable<TSource> source,
            TResult seed,
            Func<TSource, TResult, int, TResult> projection)
        {
            TResult nextSeed = seed;
            int index = 0;
            foreach (TSource src in source)
            {
                TResult projectedValue = projection(src, nextSeed, index++);
                nextSeed = projectedValue;
                yield return projectedValue;
            }
        }

        public static IEnumerable<TSource> SequenceAt<TSource>(this TSource[] source, int index)
        {
            int i = index;
            while (i < source.Length)
                yield return source[i++];
        }

        public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int count)
        {
            var saveList = new Queue<T>();
            var saved = 0;
            foreach (T item in source)
            {
                if (saved < count)
                {
                    saveList.Enqueue(item);
                    ++saved;
                    continue;
                }

                saveList.Enqueue(item);
                yield return saveList.Dequeue();
            }
        }

        public static bool? ToBoolean(this XAttribute a)
        {
            if (a == null)
                return null;

            string s = ((string) a).ToLower();
            switch (s)
            {
                case "1":
                    return true;
                case "0":
                    return false;
                case "true":
                    return true;
                case "false":
                    return false;
                case "on":
                    return true;
                case "off":
                    return false;
                default:
                    return (bool) a;
            }
        }

        private static string GetQName(XElement xe)
        {
            string prefix = xe.GetPrefixOfNamespace(xe.Name.Namespace);
            if (xe.Name.Namespace == XNamespace.None || prefix == null)
                return xe.Name.LocalName;

            return prefix + ":" + xe.Name.LocalName;
        }

        private static string GetQName(XAttribute xa)
        {
            string prefix = xa.Parent != null ? xa.Parent.GetPrefixOfNamespace(xa.Name.Namespace) : null;
            if (xa.Name.Namespace == XNamespace.None || prefix == null)
                return xa.Name.ToString();

            return prefix + ":" + xa.Name.LocalName;
        }

        private static string NameWithPredicate(XElement el)
        {
            if (el.Parent != null && el.Parent.Elements(el.Name).Count() != 1)
                return GetQName(el) + "[" +
                    (el.ElementsBeforeSelf(el.Name).Count() + 1) + "]";
            else
                return GetQName(el);
        }

        public static string StrCat<T>(this IEnumerable<T> source,
            string separator)
        {
            return source.Aggregate(new StringBuilder(),
                       (sb, i) => sb
                           .Append(i.ToString())
                           .Append(separator),
                       s => s.ToString());
        }

        public static string GetXPath(this XObject xobj)
        {
            if (xobj.Parent == null)
            {
                var doc = xobj as XDocument;
                if (doc != null)
                    return ".";

                var el = xobj as XElement;
                if (el != null)
                    return "/" + NameWithPredicate(el);

                var xt = xobj as XText;
                if (xt != null)
                    return null;

                //
                //the following doesn't work because the XPath data
                //model doesn't include white space text nodes that
                //are children of the document.
                //
                //return
                //    "/" +
                //    (
                //        xt
                //        .Document
                //        .Nodes()
                //        .OfType<XText>()
                //        .Count() != 1 ?
                //        "text()[" +
                //        (xt
                //        .NodesBeforeSelf()
                //        .OfType<XText>()
                //        .Count() + 1) + "]" :
                //        "text()"
                //    );
                //
                var com = xobj as XComment;
                if (com != null && com.Document != null)
                    return
                        "/" +
                        (
                            com
                                .Document
                                .Nodes()
                                .OfType<XComment>()
                                .Count() != 1
                                ? "comment()[" +
                                  (com
                                       .NodesBeforeSelf()
                                       .OfType<XComment>()
                                       .Count() + 1) +
                                  "]"
                                : "comment()"
                        );

                var pi = xobj as XProcessingInstruction;
                if (pi != null)
                    return
                        "/" +
                        (
                            pi.Document != null && pi.Document.Nodes().OfType<XProcessingInstruction>().Count() != 1
                                ? "processing-instruction()[" +
                                  (pi
                                       .NodesBeforeSelf()
                                       .OfType<XProcessingInstruction>()
                                       .Count() + 1) +
                                  "]"
                                : "processing-instruction()"
                        );

                return null;
            }
            else
            {
                var el = xobj as XElement;
                if (el != null)
                {
                    return
                        "/" +
                        el
                            .Ancestors()
                            .InDocumentOrder()
                            .Select(e => NameWithPredicate(e))
                            .StrCat("/") +
                        NameWithPredicate(el);
                }

                var at = xobj as XAttribute;
                if (at != null && at.Parent != null)
                    return
                        "/" +
                        at
                            .Parent
                            .AncestorsAndSelf()
                            .InDocumentOrder()
                            .Select(e => NameWithPredicate(e))
                            .StrCat("/") +
                        "@" + GetQName(at);

                var com = xobj as XComment;
                if (com != null && com.Parent != null)
                    return
                        "/" +
                        com
                            .Parent
                            .AncestorsAndSelf()
                            .InDocumentOrder()
                            .Select(e => NameWithPredicate(e))
                            .StrCat("/") +
                        (
                            com
                                .Parent
                                .Nodes()
                                .OfType<XComment>()
                                .Count() != 1
                                ? "comment()[" +
                                  (com
                                       .NodesBeforeSelf()
                                       .OfType<XComment>()
                                       .Count() + 1) + "]"
                                : "comment()"
                        );

                var cd = xobj as XCData;
                if (cd != null && cd.Parent != null)
                    return
                        "/" +
                        cd
                            .Parent
                            .AncestorsAndSelf()
                            .InDocumentOrder()
                            .Select(e => NameWithPredicate(e))
                            .StrCat("/") +
                        (
                            cd
                                .Parent
                                .Nodes()
                                .OfType<XText>()
                                .Count() != 1
                                ? "text()[" +
                                  (cd
                                       .NodesBeforeSelf()
                                       .OfType<XText>()
                                       .Count() + 1) + "]"
                                : "text()"
                        );

                var tx = xobj as XText;
                if (tx != null && tx.Parent != null)
                    return
                        "/" +
                        tx
                            .Parent
                            .AncestorsAndSelf()
                            .InDocumentOrder()
                            .Select(e => NameWithPredicate(e))
                            .StrCat("/") +
                        (
                            tx
                                .Parent
                                .Nodes()
                                .OfType<XText>()
                                .Count() != 1
                                ? "text()[" +
                                  (tx
                                       .NodesBeforeSelf()
                                       .OfType<XText>()
                                       .Count() + 1) + "]"
                                : "text()"
                        );

                var pi = xobj as XProcessingInstruction;
                if (pi != null && pi.Parent != null)
                    return
                        "/" +
                        pi
                            .Parent
                            .AncestorsAndSelf()
                            .InDocumentOrder()
                            .Select(e => NameWithPredicate(e))
                            .StrCat("/") +
                        (
                            pi
                                .Parent
                                .Nodes()
                                .OfType<XProcessingInstruction>()
                                .Count() != 1
                                ? "processing-instruction()[" +
                                  (pi
                                       .NodesBeforeSelf()
                                       .OfType<XProcessingInstruction>()
                                       .Count() + 1) + "]"
                                : "processing-instruction()"
                        );

                return null;
            }
        }
    }

    public class ExecutableRunner
    {
        public class RunResults
        {
            public int ExitCode;
            public Exception RunException;
            public StringBuilder Output;
            public StringBuilder Error;
        }

        public static RunResults RunExecutable(string executablePath, string arguments, string workingDirectory)
        {
            RunResults runResults = new RunResults
            {
                Output = new StringBuilder(),
                Error = new StringBuilder(),
                RunException = null
            };
            try
            {
                //if (File.Exists(executablePath))
                //{
                //    using (Process proc = new Process())
                //    {
                //        proc.StartInfo.FileName = executablePath;
                //        proc.StartInfo.Arguments = arguments;
                //        proc.StartInfo.WorkingDirectory = workingDirectory;
                //        proc.StartInfo.UseShellExecute = false;
                //        proc.StartInfo.RedirectStandardOutput = true;
                //        proc.StartInfo.RedirectStandardError = true;
                //        proc.OutputDataReceived +=
                //            (o, e) => runResults.Output.Append(e.Data).Append(Environment.NewLine);
                //        proc.ErrorDataReceived +=
                //            (o, e) => runResults.Error.Append(e.Data).Append(Environment.NewLine);
                //        proc.Start();
                //        proc.BeginOutputReadLine();
                //        proc.BeginErrorReadLine();
                //        proc.WaitForExit();
                //        runResults.ExitCode = proc.ExitCode;
                //    }
                //}
                //else
                //{
                //    throw new ArgumentException("Invalid executable path.", "executablePath");
                //}
            }
            catch (Exception e)
            {
                runResults.RunException = e;
            }
            return runResults;
        }
    }

    public class SiblingsReverseDocumentOrderInfo
    {
        public XElement PreviousSibling;
    }

    public class DescendantsReverseDocumentOrderInfo
    {
        public XElement PreviousElement;
    }

    public class DescendantsTrimmedReverseDocumentOrderInfo
    {
        public XElement PreviousElement;
    }

    public class GroupOfAdjacent<TSource, TKey> : IGrouping<TKey, TSource>
    {
        public GroupOfAdjacent(List<TSource> source, TKey key)
        {
            GroupList = source;
            Key = key;
        }

        public TKey Key { get; set; }
        private List<TSource> GroupList { get; set; }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable<TSource>) this).GetEnumerator();
        }

        IEnumerator<TSource> IEnumerable<TSource>.GetEnumerator()
        {
            return ((IEnumerable<TSource>) GroupList).GetEnumerator();
        }
    }

    public static class PtBucketTimer
    {
        private class BucketInfo
        {
            public int Count;
            public TimeSpan Time;
        }

        private static string LastBucket = null;
        private static DateTime LastTime;
        private static Dictionary<string, BucketInfo> Buckets;

        public static void Bucket(string bucket)
        {
            DateTime now = DateTime.Now;
            if (LastBucket != null)
            {
                TimeSpan d = now - LastTime;
                if (Buckets.ContainsKey(LastBucket))
                {
                    Buckets[LastBucket].Count = Buckets[LastBucket].Count + 1;
                    Buckets[LastBucket].Time += d;
                }
                else
                {
                    Buckets.Add(LastBucket, new BucketInfo()
                    {
                        Count = 1,
                        Time = d,
                    });
                }
            }
            LastBucket = bucket;
            LastTime = now;
        }

        public static string DumpBucketsByKey()
        {
            StringBuilder sb = new StringBuilder();
            foreach (var bucket in Buckets.OrderBy(b => b.Key))
            {
                string ts = bucket.Value.Time.ToString();
                if (ts.Contains('.'))
                    ts = ts.Substring(0, ts.Length - 5);
                string s = bucket.Key.PadRight(60, '-') + "  " + string.Format("{0:00000000}", bucket.Value.Count) + "  " + ts;
                sb.Append(s + Environment.NewLine);
            }
            TimeSpan total = Buckets
                .Aggregate(TimeSpan.Zero, (t, b) => t + b.Value.Time);
            var tz = total.ToString();
            sb.Append(string.Format("Total: {0}", tz.Substring(0, tz.Length - 5)));
            return sb.ToString();
        }

        public static string DumpBucketsByTime()
        {
            StringBuilder sb = new StringBuilder();
            foreach (var bucket in Buckets.OrderBy(b => b.Value.Time))
            {
                string ts = bucket.Value.Time.ToString();
                if (ts.Contains('.'))
                    ts = ts.Substring(0, ts.Length - 5);
                string s = bucket.Key.PadRight(60, '-') + "  " + string.Format("{0:00000000}", bucket.Value.Count) + "  " + ts;
                sb.Append(s + Environment.NewLine);
            }
            TimeSpan total = Buckets
                .Aggregate(TimeSpan.Zero, (t, b) => t + b.Value.Time);
            var tz = total.ToString();
            sb.Append(string.Format("Total: {0}", tz.Substring(0, tz.Length - 5)));
            return sb.ToString();
        }

        public static void Init()
        {
            Buckets = new Dictionary<string, BucketInfo>();
        }
    }

    public class XEntity : XText
    {
        public override void WriteTo(XmlWriter writer)
        {
            if (Value.Substring(0, 1) == "#")
            {
                string e = string.Format("&{0};", Value);
                writer.WriteRaw(e);
            }
            else
                writer.WriteEntityRef(Value);
        }

        public XEntity(string value) : base(value)
        {
        }
    }

    public static class Xsi
    {
        public static XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance";

        public static XName schemaLocation = xsi + "schemaLocation";
        public static XName noNamespaceSchemaLocation = xsi + "noNamespaceSchemaLocation";
    }
}
